﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;

namespace GerbilLib
{
    /// <summary>
    /// Gets movie or tv show meta data information from the IMDb website.
    /// </summary>
    public class ImdbScraper
    {
        #region .   Private Static Members   .
        /// <summary>
        /// Regular expression for matching the runtime of a movie or tv series
        /// </summary>
        private static Regex regexRuntime = new Regex(@"\D*(?<Runtime>\d+)");
        /// <summary>
        /// Regular expression for matching a title from a movie
        /// </summary>
        private static Regex regexMovie = new Regex(@"\(\d+\)");
        /// <summary>
        /// Regular expression for matching a title from a tv movie
        /// </summary>
        private static Regex regexTvMovie = new Regex(@"\(TV\s+\d+");
        /// <summary>
        /// Regular expression for matching a title from a video game
        /// </summary>
        private static Regex regexVideoGame = new Regex(@"\(Video Game\s+\d+");
        /// <summary>
        /// Regular expression for matching a title from a tv series
        /// </summary>
        private static Regex regexTvSeries = new Regex(@"\(TV Series\s+\d+");
        /// <summary>
        /// Regular expression for matching a title from a tv mini series
        /// </summary>
        private static Regex regexTvMiniSeries = new Regex(@"\(TV mini-series\s+\d+");
        /// <summary>
        /// Regular expression for matching a title from a video movie
        /// </summary>
        private static Regex regexVideo = new Regex(@"\(Video\s+\d+");
        #endregion

        /// <summary>
        /// Sends a web request to IMDb for the given title and downloads various
        /// pieces of meta data information such as rating, votes, description etc.
        /// </summary>
        /// <param name="item">MediaItem which will be filled with meta data. Needs to have title string set for IMDb query</param>
        /// <param name="updateMissingInfoOnly">If true only empty fields will be populated, if false all fields will be updated</param>
        /// <param name="updateVotesAndRatingOnly">If true only the Votes and Rating fields will be updated, if false all fields will be updated</param>
        public static void GetImdbMediaInfo(MediaItem item, bool updateMissingInfoOnly = false, bool updateVotesAndRatingOnly = false)
        {
            // Let's see how long this takes!
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            // Launch IMDb web request
            HtmlWeb hw = new HtmlWeb();
            HtmlDocument doc = null;
            try
            {
                if (!string.IsNullOrEmpty(item.ImdbId))
                    doc = hw.Load(string.Format("http://www.imdb.com/title/{0}/", item.ImdbId));
                else
                    doc = hw.Load(string.Format("http://www.imdb.com/find?s=all&q={0}", HttpUtility.UrlEncode(item.Title, System.Text.Encoding.Default)));
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("ImdbScraper: An exception occurred while retrieving media info: {0}", ex.Message), LoggingLevel.Error);
                return;
            }

            // If the title was not "clean" IMDb reroutes us to the results page, skipping this for now
            if (GetImdbId(hw.ResponseUri.AbsolutePath) == string.Empty)
            {
                Logger.LogMessage(string.Format("ImdbScraper: Could not retrieve media info for item {0} because title did not have a (single) result.", item.Title));
                return;
            }

            // Extract meta data information
            HtmlAttribute title = doc.DocumentNode.SelectSingleNode("//meta[@property='og:title']").Attributes["content"];
            //HtmlAttribute type = doc.DocumentNode.SelectSingleNode("//meta[@property='og:type']").Attributes["content"];
            HtmlNode year = null;
            switch (GetMediaType(HttpUtility.HtmlDecode(title.Value)))
            {
                case MediaType.Movie:
                    year = doc.DocumentNode.SelectSingleNode("//a[starts-with(@href, '/year/')]");
                    break;
                case MediaType.Series:
                    year = doc.DocumentNode.SelectSingleNode("//h1[@class='header']/span");
                    break;
            }
            HtmlNode description = doc.DocumentNode.SelectSingleNode("//div[@class='article title-overview']/p/p");
            if (description == null) // If the previous method didn't work try alternate description location
                description = doc.DocumentNode.SelectSingleNode("//td[@id='overview-top']/p/p");
            HtmlNodeCollection genres = doc.DocumentNode.SelectNodes("//a[starts-with(@href, '/genre/')]");

            // Example for Image Small: http://ia.media-imdb.com/images/M/MV5BMTgxOTY4Mjc0MF5BMl5BanBnXkFtZTcwNTA4MDQyMw@@._V1._SX99_SY140_.jpg
            // Example for Image Medium: http://ia.media-imdb.com/images/M/MV5BMTgxOTY4Mjc0MF5BMl5BanBnXkFtZTcwNTA4MDQyMw@@._V1._SY314_CR4,0,214,314_.jpg
            // Example for Image Fullsize: http://ia.media-imdb.com/images/M/MV5BMTgxOTY4Mjc0MF5BMl5BanBnXkFtZTcwNTA4MDQyMw@@._V1_.jpg
            HtmlAttribute picture = doc.DocumentNode.SelectSingleNode("//meta[@property='og:image']").Attributes["content"];
            HtmlNode runtime = doc.DocumentNode.SelectSingleNode("//div[starts-with(h4, 'Runtime')]");
            
            HtmlNode mpaaRatingImage = doc.DocumentNode.SelectSingleNode("//img[contains(@src, '/certificates/us')]");
            HtmlAttribute mpaaRating = null;
            if (mpaaRatingImage != null) mpaaRating = mpaaRatingImage.Attributes["title"];

            // Populate item meta data fields
            if (!updateVotesAndRatingOnly)
            {
                if (!updateMissingInfoOnly || string.IsNullOrEmpty(item.Title)) item.Title = HttpUtility.HtmlDecode(title.Value);
                if (!updateMissingInfoOnly || string.IsNullOrEmpty(item.Genre)) item.Genre = GetGenreString(genres);
                if (!updateMissingInfoOnly || string.IsNullOrEmpty(item.ImageBaseUrl)) item.ImageBaseUrl = GetImageUrl(picture);
                if (!updateMissingInfoOnly || string.IsNullOrEmpty(item.ImdbId)) item.ImdbId = GetImdbId(hw.ResponseUri.AbsolutePath);
                if (!updateMissingInfoOnly || item.MediaType == MediaType.Unkown) item.MediaType = GetMediaType(item.Title);
                if (!updateMissingInfoOnly || item.Year == 0) item.Year = GetYear(year, item.Title);
                if (!updateMissingInfoOnly || string.IsNullOrEmpty(item.Description)) item.Description = (description != null) ? HttpUtility.HtmlDecode(description.InnerHtml) : string.Empty;
                if (!updateMissingInfoOnly || item.Runtime == 0) item.Runtime = GetRuntime(runtime);
                if (!updateMissingInfoOnly || item.ContentRating == ContentRating.Unknown) item.ContentRating = GetContentRating(mpaaRating);
            }
            if (!updateMissingInfoOnly || item.Rating == 0) item.Rating = GetRating(doc.DocumentNode);
            if (!updateMissingInfoOnly || item.Votes == 0) item.Votes = GetVotes(doc.DocumentNode);

            // And we're done
            stopwatch.Stop();

            Logger.LogMessage(string.Format("ImdbScraper retrieved info in {7:F} seconds. The type of the IMDb title is {0} and the title is {1} from the year {5}, rated {2}/10 with {3} votes. Genres are {6}. The description is: \"{4}\"",
                item.MediaType,
                HttpUtility.HtmlDecode(title.Value),
                item.Rating,
                (item.Votes != 0) ? item.Votes.ToString() : "no",
                (description != null) ? HttpUtility.HtmlDecode(description.InnerHtml) : "(none yet)",
                item.Year,
                item.Genre,
                stopwatch.ElapsedMilliseconds / 1000.0
            ), LoggingLevel.Debug);
        }

        /// <summary>
        /// Extracts the image base url for an IMDb hosted image. The result will not actually download a picture
        /// but can be used as the base to get small, medium, or full size images from IMDb.
        /// </summary>
        /// <param name="imageSrc">Image URL attribute as extracted from IMDb web response</param>
        /// <returns>String with base url for an IMDb image</returns>
        private static string GetImageUrl(HtmlAttribute imageSrc)
        {
            if (imageSrc == null || string.IsNullOrEmpty(imageSrc.Value)) return string.Empty;
            int index = imageSrc.Value.LastIndexOf("_V1");
            if (index != -1)
            {
                string prefix = imageSrc.Value.Substring(0, index);
                if (!string.IsNullOrEmpty(prefix.Trim()))
                    return string.Format("{0}_V1", prefix);
            }

            return string.Empty;
        }

        /// <summary>
        /// Determines the media type of the queried title as returned by an IMDb web response.
        /// The OpenGraph.org values for media types was unrealiable because IMDb miscategorizes
        /// TV Movies as TV Series, so we are relying on parsing the title string information instead.
        /// </summary>
        /// <param name="input">Title information as extracted from IMDb web response</param>
        /// <returns>Corresponding value from MediaType enumeration</returns>
        private static MediaType GetMediaType(string input)
        {
            if (string.IsNullOrWhiteSpace(input)) return MediaType.Unkown;

            if (regexMovie.IsMatch(input)) return MediaType.Movie;
            else if (regexTvMovie.IsMatch(input)) return MediaType.TvMovie;
            else if (regexTvSeries.IsMatch(input)) return MediaType.Series;
            else if (regexTvMiniSeries.IsMatch(input)) return MediaType.MiniSeries;
            else if (regexVideo.IsMatch(input)) return MediaType.VideoMovie;
            else if (regexVideoGame.IsMatch(input)) return MediaType.VideoGame;
            
            return MediaType.Unkown;
        }

        /// <summary>
        /// Retrieves the IMDb title ID "ttxxxxxx" from the response URI.
        /// </summary>
        /// <param name="uri">Web response URI as returned by an IMDb request</param>
        /// <returns>IMDb title ID</returns>
        private static string GetImdbId(string uri)
        {
            string[] parts = uri.Split('/');
            if (parts.Length > 2) return parts[2];
            else return string.Empty;
        }

        /// <summary>
        /// Gets all the genres listed on the IMDb web page for the title. A genre is picked
        /// up if there is a link to a genre category attached to it. Duplicates will be
        /// filtered out.
        /// </summary>
        /// <param name="genres">Collection of html nodes with genre links</param>
        /// <returns>String of comma-separated list of genres</returns>
        private static string GetGenreString(HtmlNodeCollection genres)
        {
            if (genres == null || genres.Count == 0) return string.Empty;

            StringBuilder genreString = new StringBuilder();
            foreach (HtmlNode genre in genres)
            {
                string text = HttpUtility.HtmlDecode(genre.InnerHtml);
                if (!genreString.ToString().Contains(text)) genreString.AppendFormat("{0}, ", text);
            }
            if (genreString.Length > 2) genreString.Remove(genreString.Length - 2, 2);
            return genreString.ToString();
        }

        /// <summary>
        /// Returns the numeric integer value of the content of an Html node.
        /// </summary>
        /// <param name="node">Html node containing an integer value</param>
        /// <returns>Integer value of Html node text or 0 if not a number</returns>
        private static int GetValue(HtmlNode node)
        {
            if (node == null) return 0;
            return GetValue(node.InnerText);
        }

        /// <summary>
        /// Returns the numeric integer value of the content of the string or
        /// 0 if it is not a valid number.
        /// </summary>
        /// <param name="input">String containing an integer value</param>
        /// <returns>Integer value of string or 0 if not a number</returns>
        private static int GetValue(string input)
        {
            int value = 0;
            if (input != null) int.TryParse(input, out value);
            return value;
        }

        /// <summary>
        /// Returns the number of votes from an IMDb title web response.
        /// </summary>
        /// <param name="votes">HtmlNode containing the votes string</param>
        /// <returns>Number of votes</returns>
        private static int GetVotes(HtmlNode documentNode)
        {
            HtmlNode votes = documentNode.SelectSingleNode("//a[@href='ratings']");

            if (votes == null) return 0;
            // TODO: Make this culture-aware for future support of other language versions of IMDb
            return GetValue(votes.InnerText.Split(' ')[0].Replace("votes", string.Empty).Replace(",", string.Empty).Trim());
        }

        /// <summary>
        /// Returns the production year value for an IMDb title web response.
        /// </summary>
        /// <param name="year">HtmlNode containing the production year information.</param>
        /// <param name="title">Title of the IMDb item.</param>
        /// <returns>Production year of title</returns>
        private static int GetYear(HtmlNode year, string title)
        {
            if (year == null && string.IsNullOrEmpty(title)) return 0;

            // Try parsing the easy way first, if it succeeds we can stop
            int result = GetValue(year);
            if (result != 0) return result;

            // Try extracting the year from the title string
            GroupCollection gc = MediaItem.RegexLastYear.Match(title).Groups;
            result = GetValue(gc[0].Value);
            return result;
        }

        /// <summary>
        /// Returns the IMDb title rating xx out of 10.
        /// </summary>
        /// <param name="attribute">HtmlAttribute containing the rating information</param>
        /// <returns>IMDb title rating value</returns>
        private static float GetRating(HtmlNode documentNode)
        {
            HtmlNode ratingNode = documentNode.SelectSingleNode("//span[@itemprop='ratingValue']");
            float rating = 0f;

            if (ratingNode != null)
            {
                //string[] parts = attribute.Value.Split('|');
                // Relies on rating info to be the third item in the array
                //if (parts.Length > 2) float.TryParse(parts[2], out rating);
                float.TryParse(ratingNode.InnerText, out rating);
            }
            return rating;
        }

        /// <summary>
        /// Downloads and saves a cover image from the IMDb image server using the image
        /// link provided in the IMDb meta data info for a title.
        /// </summary>
        /// <param name="item">Media item for which to download the cover image</param>
        /// <returns>True if download was successful, otherwise false</returns>
        public static bool SaveCoverImageToFolder(MediaItem item)
        {
            if (!string.IsNullOrEmpty(item.ImageBaseUrl))
            {
                string path = string.Format("{0}{1}", item.Path, MediaItem.DefaultCoverImageFilename);
                FileDownloader download = new FileDownloader(new Uri(item.GetPosterImageUrl()));
                download.Download(path);
                item.HasCoverImage = true;

                return true;
            }
            
            return false;
        }

        /// <summary>
        /// Determines the runtime of a movie or tv show title as returned by an IMDb web response.
        /// </summary>
        /// <param name="node">IMDb web response node containing the runtime value</param>
        /// <returns>Runtime value in full minutes or 0 if unknown</returns>
        private static int GetRuntime(HtmlNode node)
        {
            if (node == null) return 0;

            string text = node.InnerText.Replace("\n", string.Empty);
            GroupCollection gc = regexRuntime.Match(text).Groups;
            return GetValue(gc["Runtime"].Value);
        }

        /// <summary>
        /// Determines the correct value from the ContentRating enumeration
        /// that matches the value from the input string.
        /// </summary>
        /// <param name="attribute">Ratings attribute as extracted from an IMDb web response</param>
        /// <returns>Corresponding Content Rating or Unknown if no match can be made</returns>
        private static ContentRating GetContentRating(HtmlAttribute attribute)
        {
            if (attribute == null) return ContentRating.Unknown;

            switch(attribute.Value)
            {
                case "G": return ContentRating.G;
                case "R": return ContentRating.R;
                case "PG_13": return ContentRating.PG13;
                case "PG": return ContentRating.PG;
                case "NC_17": return ContentRating.NC17;
                case "UNRATED": return ContentRating.Unrated;
                case "TV_G": return ContentRating.TV_G;
                case "TV_PG": return ContentRating.TV_PG;
                case "TV_Y": return ContentRating.TV_Y;
                case "TV_Y7": return ContentRating.TV_Y7;
                case "TV_14": return ContentRating.TV14;
                case "TV_MA": return ContentRating.TV_MA;
                default: return ContentRating.Unknown;
            }
        }

        /// <summary>
        /// Retrieves the list of suggested title matches as returned by the IMDb Advanced Title Search.
        /// The list is returned as a collection of MediaItems with their title, IMDb ID and Image URL
        /// properties set so the Title Match dialog can display a thumbnail cover image.
        /// </summZary>
        /// <param name="input">Title to use as a search string</param>
        /// <param name="titleCleaningOptions">Title cleaning options typically configured in the Preferences</param>
        /// <returns>List of potential matches</returns>
        public static List<IMediaItem> GetImdbAdvancedTitleSearchMatches(string input, MediaType mediaType, TitleCleaningOptions titleCleaningOptions)
        {
            // Let's see how long this takes!
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            // Clean up input as the IMDb Advanced Title Search gets thrown off if the title contains the year of the movie
            // unlike the regular IMDb search which deals fine with year in the title. This might cause less than exact
            // matches for movies that actually have a number in their title.
            string originalInput = input;
            TitleCleaningOptions tco = new TitleCleaningOptions(titleCleaningOptions.IgnorePatterns, stripYearFromTitle: true, replacePeriodsWithSpaces: titleCleaningOptions.ReplacePeriodsWithSpaces);
            input = MediaItem.GetCleanTitle(input, tco);

            // Launch IMDb web request
            HtmlWeb hw = new HtmlWeb();
            HtmlDocument doc = null;
            try
            {
                string titleType = mediaType == MediaType.Movie ? "feature" : "tv_series";
                // TODO: Use compact view (?view=simple&) instead but need to filter by <td class="title"> instead
                doc = hw.Load(string.Format("http://www.imdb.com/search/title?title_type={0}&title={1}", titleType, HttpUtility.UrlEncode(input)));
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("ImdbScraper: An exception occurred while retrieving title matches: {0}", ex.Message), LoggingLevel.Error);
                return null;
            }

            List<IMediaItem> items = new List<IMediaItem>();

            // Extract meta data information
            HtmlNodeCollection titleMatches = doc.DocumentNode.SelectNodes("//a[starts-with(@href, '/title/')]");
            if (titleMatches != null)
                foreach (HtmlNode titleMatch in titleMatches)
                {
                    if (titleMatch.Attributes["title"] == null) continue;
                    string title = titleMatch.Attributes["title"].Value;
                    if (title == "Delete") continue;
                    string imdbId = GetImdbId(titleMatch.Attributes["href"].Value);
                    HtmlAttribute imageSrc = titleMatch.SelectSingleNode(string.Format("img[@alt = '{0}']", title)).Attributes["src"];
                    items.Add(new MediaItem(title: HttpUtility.HtmlDecode(title), imdbId: imdbId, imageUrl: GetImageUrl(imageSrc)));
                }

            // And we're done
            stopwatch.Stop();
            Logger.LogMessage(string.Format("ImdbScraper retrieved {0} title matches for {1} in {2:F} seconds.", items.Count, originalInput, stopwatch.ElapsedMilliseconds / 1000.0));
            return items;
        }
    }
}
